package com.demo.controller;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.demo.common.ResultData;
import com.demo.config.HttpConfig;
import com.demo.filter.RequestContextHelper;
import com.demo.kyc.KycConfig;
import com.demo.kyc.model.*;
import com.demo.util.IpUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okhttp3.*;
import okio.Buffer;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * sumsub kyc
 *
 * @author wangfengchen
 * <a href="https://docs.sumsub.com/reference/about-sumsub-api">https://docs.sumsub.com/reference/about-sumsub-api</a>
 * <a href="https://github.com/SumSubstance/AppTokenUsageExamples">https://github.com/SumSubstance/AppTokenUsageExamples</a>
 */
@Slf4j
@RestController
@RequestMapping("/kyc")
public class KycController {


  @Autowired
  private KycConfig kycConfig;
  @Autowired
  private HttpConfig httpConfig;


  /**
   * webhook 回调
   *
   * @param data
   * @param response
   */
  @PostMapping("/callback")
  public void callback(@org.springframework.web.bind.annotation.RequestBody String data, HttpServletRequest request, HttpServletResponse response) {
    String requestId = RequestContextHelper.get().getRequestId();
    String digest = request.getHeader("x-payload-digest");
    String digestAlg = request.getHeader("X-Payload-Digest-Alg");
    log.info("requestId = {}，kyc回调开始，ip = {}，digest = {}，digestAlg = {}，data = {}", requestId, IpUtil.getIpAddr(request), digest, digestAlg, data);
    if (StringUtils.isBlank(digest) || StringUtils.isBlank(digestAlg)) {
      log.error("requestId = {}，kyc回调缺少请求头", requestId);
      response(HttpStatus.OK, response);
      return;
    }
    if (!verifyDigest(digest, data)) {
      log.error("requestId = {}，鉴权失败", requestId);
      response(HttpStatus.OK, response);
      return;
    }
    log.info("requestId = {}，鉴权成功", requestId);
    WebHookResult webHookResult = JSON.parseObject(data, WebHookResult.class);
    // 如果不是最终态或者不符合levelName则直接返回
    if (webHookResult == null || !webHookResult.getReviewStatus().equals("completed") || !webHookResult.getLevelName().equals(kycConfig.getLevelName())) {
      log.info("requestId = {}，不符合业务规则直接返回，webHookResult = {}", requestId, JSON.toJSONString(webHookResult));
      response(HttpStatus.OK, response);
      return;
    }
    // todo 业务
    response(HttpStatus.OK, response);
  }


  /**
   * 响应结果
   *
   * @param httpStatus 响应状态
   * @param response   http响应请求
   */
  void response(HttpStatus httpStatus, HttpServletResponse response) {
    PrintWriter printWriter;
    try {
      printWriter = response.getWriter();
      printWriter.print(httpStatus);
      printWriter.flush();
      printWriter.close();
    } catch (IOException e) {
      log.error("响应结果出错", e);
    }
  }

  /**
   * 生成签名
   *
   * @return
   */
  @PostMapping("/testDigest")
  public ResultData testDigest(@org.springframework.web.bind.annotation.RequestBody String data) {
    MediaType mediaType = MediaType.parse("text/plain");
    RequestBody body = RequestBody.create(mediaType, data.getBytes(StandardCharsets.UTF_8));
    String urlTemplate = "/resources/inspectionCallbacks/testDigest?secretKey={}&digestAlg=HMAC_SHA256_HEX";
    String url = StrUtil.format(urlTemplate, kycConfig.getWebHookKey());
    String response = sendPost(url, body);
    return ResultData.success(response);
  }

  /**
   * 生成签名
   *
   * @return
   */
  @PostMapping("/testDigest2")
  public ResultData testDigest2(HttpServletRequest request) {
    MediaType mediaType = MediaType.parse("text/plain");
    String data = readAsChars(request);
    RequestBody body = RequestBody.create(mediaType, data);
    String urlTemplate = "/resources/inspectionCallbacks/testDigest?secretKey={}&digestAlg=HMAC_SHA256_HEX";
    String url = StrUtil.format(urlTemplate, kycConfig.getWebHookKey());
    String response = sendPost(url, body);
    return ResultData.success(response);
  }

  public static String readAsChars(HttpServletRequest request) {
    BufferedReader br = null;
    StringBuilder sb = new StringBuilder("");
    try {
      br = request.getReader();
      String str;
      while ((str = br.readLine()) != null) {
        sb.append(str);
      }
      br.close();
    } catch (IOException e) {
      log.error("从request读取信息错误", e);
    } finally {
      if (null != br) {
        try {
          br.close();
        } catch (IOException e) {
          log.error("http stream close error", e);
        }
      }
    }
    return sb.toString();
  }

  /**
   * 校验签名
   *
   * @return
   */
  @PostMapping("/checkDigest")
  public ResultData checkDigest(@RequestParam String digest, @org.springframework.web.bind.annotation.RequestBody String data) {
    return ResultData.success(verifyDigest(digest, data));
  }

  /**
   * 生成访问令牌（查询参数）
   *
   * @return
   */
  @GetMapping("/getAccessToken")
  public ResultData getAccessToken(@RequestParam Long userId) {
//    MediaType mediaType = MediaType.parse("application/json");
//    RequestBody body = RequestBody.create(mediaType, "{\"idType\":\"OTHER\",\"country\":\"AUS\"}");
    RequestBody body = RequestBody.create(null, new byte[0]);
    String urlTemplate = "/resources/accessTokens?userId={}&levelName={}&ttlInSecs={}";
    String url = null;
    try {
      url = StrUtil.format(urlTemplate, URLEncoder.encode(String.valueOf(userId), StandardCharsets.UTF_8.toString()), URLEncoder.encode(kycConfig.getLevelName(), StandardCharsets.UTF_8.toString()), kycConfig.getExpire());
    } catch (UnsupportedEncodingException e) {
      log.error("exception = {}", ExceptionUtils.getStackTrace(e));
      throw new RuntimeException(e);
    }
    String response = sendPost(url, body);
    return ResultData.success(response);
  }

  /**
   * 创建申请人
   *
   * @return
   */
  @GetMapping("/createApplicant")
  public ResultData createApplicant(@RequestParam Long userId) {
    MediaType mediaType = MediaType.parse("application/json");
    Applicant applicant = new Applicant(String.valueOf(userId));
    RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(applicant));
    String urlTemplate = "/resources/applicants?levelName={}";
    String url = null;
    try {
      url = StrUtil.format(urlTemplate, URLEncoder.encode(kycConfig.getLevelName(), StandardCharsets.UTF_8.toString()));
    } catch (UnsupportedEncodingException e) {
      log.error("exception = {}", ExceptionUtils.getStackTrace(e));
      throw new RuntimeException(e);
    }
    String response = sendPost(url, body);
    Applicant applicantResult = JSON.parseObject(response, Applicant.class);
    return ResultData.success(applicantResult);
  }

  /**
   * 添加身份证件
   *
   * @return
   */
  @GetMapping("/addDocument")
  public ResultData addDocument(@RequestParam String applicantId) {
    OkHttpClient okHttpClient = httpConfig.getOkHttpClient();
    String urlTemplate = "/resources/applicants/{}/info/idDoc";
    String url = StrUtil.format(urlTemplate, applicantId);
    String basePath = null;
    try {
      basePath = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX).getPath() + "images" + File.separator;
    } catch (FileNotFoundException e) {
      log.error("获取文件路径异常，exception = {}", ExceptionUtils.getStackTrace(e));
      return ResultData.fail();
    }
    String filePath = basePath + "sumsub-logo.png";
    File file = new File(filePath);
    MediaType mediaType = MediaType.parse("image/*");
    RequestBody body = RequestBody.create(mediaType, file);
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("metadata", JSON.toJSONString(new Metadata(DocType.PASSPORT, "DEU")))
        .addFormDataPart("content", file.getName(), body)
        .build();

    long ts = Instant.now().getEpochSecond();

    Request request = new Request.Builder()
        .url(kycConfig.getBaseUrl() + url)
        .header("X-App-Token", kycConfig.getToken())
        .header("X-App-Access-Sig", createSignature(ts, HttpMethod.POST, url, requestBodyToBytes(requestBody)))
        .header("X-App-Access-Ts", String.valueOf(ts))
        .post(requestBody)
        .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
      String resp = response.headers().get("X-Image-Id");
      log.info("response = {}", resp);
      return ResultData.success(resp);
    } catch (Exception e) {
      log.error("Exception = {}", ExceptionUtils.getStackTrace(e));
    }
    return ResultData.fail();
  }

  /**
   * 获取申请人数据
   *
   * @return
   */
  @GetMapping("/getApplicants")
  public ResultData getApplicants(@RequestParam String applicantId) {
    String urlTemplate = "/resources/applicants/{}/one";
    String url = StrUtil.format(urlTemplate, applicantId);
    String response = sendGet(url);
    KycApplicantData kycApplicantData = JSON.parseObject(response, KycApplicantData.class);
    return ResultData.success(kycApplicantData);
  }

  /**
   * 获取申请人数据
   *
   * @return
   */
  @GetMapping("/getApplicantsByUserId")
  public ResultData getApplicantsByUserId(@RequestParam String userId) {
    String urlTemplate = "/resources/applicants/-;externalUserId={}/one";
    String url = StrUtil.format(urlTemplate, userId);
    String response = sendGet(url);
    return ResultData.success(response);
  }

  /**
   * 获取申请人审核状态
   *
   * @return
   */
  @GetMapping("/getReviewStatus")
  public ResultData getReviewStatus(@RequestParam String applicantId) {
    String urlTemplate = "/resources/applicants/{}/status";
    String url = StrUtil.format(urlTemplate, applicantId);
    String response = sendGet(url);
    return ResultData.success(response);
  }

  /**
   * 获取申请人验证步骤状态
   *
   * @return
   */
  @GetMapping("/getApplicantStatus")
  public ResultData getApplicantStatus(@RequestParam String applicantId) {
    String urlTemplate = "/resources/applicants/{}/requiredIdDocsStatus";
    String url = StrUtil.format(urlTemplate, applicantId);
    String response = sendGet(url);
    return ResultData.success(response);
  }

  /**
   * 查询拒绝原因
   *
   * @return
   */
  @GetMapping("/getRejectReason")
  public ResultData getRejectReason(@RequestParam String applicantId) {
    String urlTemplate = "/resources/moderationStates/-;applicantId={}";
    String url = StrUtil.format(urlTemplate, applicantId);
    String response = sendGet(url);
    return ResultData.success(response);
  }

  /**
   * 重置申请人资料
   *
   * @return
   */
  @GetMapping("/resetApplicant")
  public ResultData resetApplicant(@RequestParam String applicantId) {
    String urlTemplate = "/resources/applicants/{}/reset";
    String url = StrUtil.format(urlTemplate, applicantId);
    RequestBody body = RequestBody.create(null, new byte[0]);
    String response = sendPost(url, body);
    return ResultData.success(response);
  }

  /**
   * 获取图像集合
   *
   * @param applicantId
   * @return
   */
  @GetMapping("/getFileList")
  public ResultData getFileList(@RequestParam String applicantId) {
    ResultData resultData = getApplicants(applicantId);
    if (resultData.getData() == null) {
      return null;
    }
    KycApplicantData kycApplicantData = (KycApplicantData) resultData.getData();
    String inspectionId = kycApplicantData.getInspectionId();
    String fileInfo = getFileInfo(applicantId);
    if (StringUtils.isBlank(fileInfo)) {
      return null;
    }
    JSONObject jsonObject = JSON.parseObject(fileInfo);
    String items = jsonObject.getString("items");
    if (StringUtils.isBlank(items)) {
      return null;
    }
    List<FileItem> fileItems = JSON.parseArray(items, FileItem.class);
    List<FileInfo> fileInfoList = new ArrayList<>();
    for (FileItem item : fileItems) {
      if (item.getReviewResult() != null && item.getReviewResult().getReviewAnswer().equals("GREEN")) {
        FileInfo info = new FileInfo();
        info.setId(item.getId());
        info.setType(item.getIdDocDef().getIdDocType());
        info.setSubType(item.getIdDocDef().getIdDocSubType());
        info.setInspectionId(inspectionId);
        fileInfoList.add(info);
      }
    }
    return ResultData.success(fileInfoList);
  }

  /**
   * 获取图像信息
   * <a href="https://docs.sumsub.com/reference/get-information-about-document-images">https://docs.sumsub.com/reference/get-information-about-document-images</a>
   *
   * @param applicantId
   * @return
   */
  @GetMapping("/getFileInfo")
  public String getFileInfo(String applicantId) {
    String urlTemplate = "/resources/applicants/{}/metadata/resources";
    String url = StrUtil.format(urlTemplate, applicantId);
    return sendGet(url);
  }

  /**
   * 获取图像文件
   *
   * @param inspectionId
   * @param imageId
   * @return
   */
  @GetMapping("/getFile")
  public String getFile(@RequestParam String inspectionId, @RequestParam String imageId) {
    String urlTemplate = "/resources/inspections/{}/resources/{}";
    String url = StrUtil.format(urlTemplate, inspectionId, imageId);
    sendGetFile(url);
    return null;
  }

  /**
   * 获取图像信息
   * <a href="https://docs.sumsub.com/reference/get-document-images">https://docs.sumsub.com/reference/get-document-images</a>
   *
   * @param inspectionId
   * @param imageId
   * @return
   */
  @GetMapping("/getFileBase64")
  public String getFileBase64(@RequestParam String inspectionId, @RequestParam String imageId) {
    String urlTemplate = "/resources/inspections/{}/resources/{}";
    String url = StrUtil.format(urlTemplate, inspectionId, imageId);
    return sendGetFileBase64(url);
  }

  private String sendGetFile(String url) {
    long ts = Instant.now().getEpochSecond();
    Request request = new Request.Builder()
        .url(kycConfig.getBaseUrl() + url)
        .header("X-App-Token", kycConfig.getToken())
        .header("X-App-Access-Sig", createSignature(ts, HttpMethod.GET, url, null))
        .header("X-App-Access-Ts", String.valueOf(ts))
        .get()
        .build();

    OkHttpClient okHttpClient = httpConfig.getOkHttpClient();
    try (Response response = okHttpClient.newCall(request).execute()) {
      ResponseBody responseBody = response.body();
      if (responseBody != null && "image/jpeg".equals(Objects.requireNonNull(responseBody.contentType()).toString())) {
        File file = new File("/Users/wangfengchen/Downloads/test/downloaded_image.jpg");
        try (InputStream inputStream = responseBody.byteStream();
             FileOutputStream outputStream = new FileOutputStream(file)) {
          byte[] buffer = new byte[4096];
          int bytesRead;
          while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
          }
          log.info("文件保存成功，path = {}", file.getAbsolutePath());
        }
      }
    } catch (Exception e) {
      log.error("Exception = {}", ExceptionUtils.getStackTrace(e));
    }
    return null;
  }

  private String sendGetFileBase64(String url) {
    long ts = Instant.now().getEpochSecond();
    Request request = new Request.Builder()
        .url(kycConfig.getBaseUrl() + url)
        .header("X-App-Token", kycConfig.getToken())
        .header("X-App-Access-Sig", createSignature(ts, HttpMethod.GET, url, null))
        .header("X-App-Access-Ts", String.valueOf(ts))
        .get()
        .build();

    OkHttpClient okHttpClient = httpConfig.getOkHttpClient();
    try (Response response = okHttpClient.newCall(request).execute()) {
      ResponseBody responseBody = response.body();
      if (responseBody != null) {
        byte[] fileBytes = responseBody.bytes();
        return Base64.getEncoder().encodeToString(fileBytes);
      }
    } catch (Exception e) {
      log.error("Exception = {}", ExceptionUtils.getStackTrace(e));
    }
    return null;
  }

  /**
   * 发送get请求
   *
   * @param url
   * @return
   */
  private String sendGet(String url) {
    long ts = Instant.now().getEpochSecond();
    Request request = new Request.Builder()
        .url(kycConfig.getBaseUrl() + url)
        .header("X-App-Token", kycConfig.getToken())
        .header("X-App-Access-Sig", createSignature(ts, HttpMethod.GET, url, null))
        .header("X-App-Access-Ts", String.valueOf(ts))
        .get()
        .build();

    OkHttpClient okHttpClient = httpConfig.getOkHttpClient();
    try (Response response = okHttpClient.newCall(request).execute()) {
      ResponseBody responseBody = response.body();
      String resp = responseBody == null ? null : responseBody.string();
      log.info("response = {}", resp);
      if (response.code() == 200 || response.code() == 201) {
        return resp;
      }
    } catch (Exception e) {
      log.error("Exception = {}", ExceptionUtils.getStackTrace(e));
    }
    return null;
  }

  /**
   * 发送post请求
   *
   * @param url
   * @param requestBody
   * @return
   */
  private String sendPost(String url, RequestBody requestBody) {
    long ts = Instant.now().getEpochSecond();
    Request request = new Request.Builder()
        .url(kycConfig.getBaseUrl() + url)
        .header("X-App-Token", kycConfig.getToken())
        .header("X-App-Access-Sig", createSignature(ts, HttpMethod.POST, url, requestBodyToBytes(requestBody)))
        .header("X-App-Access-Ts", String.valueOf(ts))
        .post(requestBody)
        .build();

    OkHttpClient okHttpClient = httpConfig.getOkHttpClient();
    try (Response response = okHttpClient.newCall(request).execute()) {
      ResponseBody responseBody = response.body();
      String resp = responseBody == null ? null : responseBody.string();
      log.info("response = {}", resp);
      if (response.code() == 200 || response.code() == 201) {
        return resp;
      }
    } catch (Exception e) {
      log.error("Exception = {}", ExceptionUtils.getStackTrace(e));
    }
    return null;
  }

  private String createSignature(long ts, HttpMethod httpMethod, String path, byte[] body) {
    Mac hmacSha256 = null;
    try {
      hmacSha256 = Mac.getInstance("HmacSHA256");
      hmacSha256.init(new SecretKeySpec(kycConfig.getSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
      log.error("exception = {}", ExceptionUtils.getStackTrace(e));
      throw new RuntimeException(e);
    }
    hmacSha256.update((ts + httpMethod.name() + path).getBytes(StandardCharsets.UTF_8));
    byte[] bytes = body == null ? hmacSha256.doFinal() : hmacSha256.doFinal(body);
    return Hex.encodeHexString(bytes);
  }

  private String createSignature(String secretKey, byte[] body) {
    return SecureUtil.hmacSha256(secretKey.getBytes()).digestHex(body);
  }

  public byte[] requestBodyToBytes(RequestBody requestBody) {
    Buffer buffer = new Buffer();
    try {
      requestBody.writeTo(buffer);
    } catch (IOException e) {
      log.error("exception = {}", ExceptionUtils.getStackTrace(e));
      throw new RuntimeException(e);
    }
    return buffer.readByteArray();
  }

  /**
   * 验证 Webhook 发件人
   *
   * @param digest
   * @param data
   * @return
   */
  private boolean verifyDigest(String digest, String data) {
    String signature = createSignature(kycConfig.getWebHookKey(), data.getBytes(StandardCharsets.UTF_8));
    log.info("signature = {}", signature);
    return StringUtils.equals(digest, signature);
  }

  /**
   * 去掉空格、换行、回车和制表符
   *
   * @param str
   * @return
   */
  public static String formatStr(String str) {
    if (str != null && str.length() > 0) {
      Pattern p = Pattern.compile("\\s*|\n|\r|\t");
      Matcher m = p.matcher(str);
      return m.replaceAll("");
    }
    return str;
  }


}
